package org.jpropeller.view.table.impl;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.MatteBorder;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;

import org.jpropeller.concurrency.CancellableResponse;
import org.jpropeller.properties.Prop;
import org.jpropeller.properties.exception.ReadOnlyException;
import org.jpropeller.properties.list.selection.ListAndSelectionAndValueReference;
import org.jpropeller.properties.list.selection.MultiSelectionReference;
import org.jpropeller.task.Task;
import org.jpropeller.task.impl.BuildTask;
import org.jpropeller.task.impl.SynchronousTaskExecutor;
import org.jpropeller.ui.impl.JDropDownButton;
import org.jpropeller.ui.impl.NimbusDropDownButton;
import org.jpropeller.view.CompletionException;
import org.jpropeller.view.JView;
import org.jpropeller.view.View;
import org.jpropeller.view.impl.CompositeViewHelper;
import org.jpropeller.view.impl.LabelView;
import org.jpropeller.view.table.TableRowView;

public class MultiSelectionDropDownListView<T> implements JView {

	
	private final CompositeViewHelper helper;
	private final JToggleButton button;
	private MultiSelectionReference<T> reference;
	private final JTable table;
	
	private final SynchronousTaskExecutor exe;
	
	protected Task makeTask(final MultiSelectionReference<T> reference)
	{
		Task task=BuildTask.on(reference).withResponse(new CancellableResponse(){

			@Override
			public void respond(AtomicBoolean shouldCancel) {
				if(reference.value()!=null && reference.value().get()!=null && reference.value().get().isEmpty())
				{
					if(reference.selection()!=null && reference.selection().get()!=null && !reference.selection().get().isEmpty())
					{
						//a PathProp may behave as if it is read-only
						//see PathProp.set
						//"Cannot look up property via path - PathProp is hence read only"
						//this is true even if the path prop claims to be editable - checking for editability does not help
						//this is why we need to catch the exception
						try{
							reference.selection().get().clear();
						}catch(ReadOnlyException e){}
					}
				}
				else
				{
					boolean emptyOrOutOfRange=false;
					emptyOrOutOfRange|=(reference.selection()!=null && reference.selection().get()!=null && reference.selection().get().isEmpty());
					if(reference.selection()!=null && reference.selection().get()!=null && reference.value()!=null && reference.value().get()!=null)
					{
						int max=reference.value().get().size();
						for(Integer i : reference.selection().get())
						{
							if(i>=max)
							{
								emptyOrOutOfRange=true;
								break;
							}
						}
					}
					if(emptyOrOutOfRange)
					{
						Set<Integer> newContents=new TreeSet<Integer>();
						newContents.add(0);
						try{
							reference.selection().get().replace(newContents);
						}catch(ReadOnlyException e){}
					}
				}
			}});
		return task;
	}
	
	public MultiSelectionDropDownListView(Class<T> modelClass,final MultiSelectionReference<T> reference,
			TableRowView<? super T> rowView, int minWidth, final int maxHeight, boolean displayHeader,
			final Prop<String> description)
	{
		this(modelClass,reference,rowView,minWidth,maxHeight,displayHeader,description,null);
	}
	
    private static java.awt.Dimension getPreferredSize(JLabel resizer,String html,boolean width, int prefSize) {
    	 
        resizer.setText(html);
 
        javax.swing.text.View view = (javax.swing.text.View) resizer.getClientProperty(
                javax.swing.plaf.basic.BasicHTML.propertyKey);
 
        view.setSize(width?prefSize:0,width?0:prefSize);
 
        float w = view.getPreferredSpan(javax.swing.text.View.X_AXIS);
        float h = view.getPreferredSpan(javax.swing.text.View.Y_AXIS);
 
        return new java.awt.Dimension((int) Math.ceil(w),
                (int) Math.ceil(h));
    }
	
	public MultiSelectionDropDownListView(Class<T> modelClass,final MultiSelectionReference<T> reference,
			TableRowView<? super T> rowView, int minWidth, final int maxHeight, boolean displayHeader,
			final Prop<String> description,final String southComponent)
	{
		List<View> views = new ArrayList<View>();
		
		this.reference = reference;
		final MultiSelectionListTableView listView = MultiSelectionListTableView.create(reference, rowView);
		views.add(listView);
		table = listView.getComponent();
		
		this.exe=new SynchronousTaskExecutor(makeTask(reference));
		
		//TODO would be nice to specify a renderer in constructor
		//Make a view of selected item from list as a label
		LabelView labelView = new LabelView(description);
		views.add(labelView);

		//TODO neater (automatic in button?) choosing between styles.
		if (NimbusDropDownButton.isNimbusAvailable()) {
			button = new NimbusDropDownButton();
		} else {
			button = new JDropDownButton();			
		}
		
		button.add(labelView.getComponent());

		final JScrollPane listScroll = new JScrollPane(listView.getComponent());
		listScroll.setBorder(null);
		final JPanel jp=new JPanel(new BorderLayout());
		jp.add(listScroll,BorderLayout.CENTER);
		final JLabel label=(southComponent==null)?null:new JLabel();
		if(southComponent!=null)
		{
			label.setText(southComponent);
			label.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.BLACK));
			jp.add(label,BorderLayout.SOUTH);
		}

		Runnable preparePopup = new Runnable() {
			@Override
			public void run() {
				int w=button.getSize().width;
				Dimension size = table.getPreferredSize();
				if(southComponent!=null)
				{
					if(southComponent.startsWith("<html>"))
					{
						size.height+=getPreferredSize(label,southComponent,true, w).height+2;
					}
					else
					{
						size.height+=label.getPreferredSize().height+2;
					}
				}
				int preferredHeight = size.height;
				if (preferredHeight > maxHeight) {
					preferredHeight = maxHeight;
				}
				//Widths will be replaced by the popup handler
				jp.setPreferredSize(new Dimension(10, preferredHeight));
				jp.setMaximumSize(new Dimension(10, preferredHeight));
				jp.setMinimumSize(new Dimension(10, preferredHeight));
			}
		};

		final PopupHandler handler = new PopupHandler(jp, listView.getComponent(), button, minWidth, preparePopup);

		
		button.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (button.isSelected()) {
					handler.show();
				}
			}
		});
		
		JTable table = listView.getComponent();
		table.addKeyListener(new KeyAdapter() {
			@Override
			public void keyTyped(KeyEvent e) {
				SwingUtilities.invokeLater(new Runnable() {
					@Override
					public void run() {
						handler.hide();
					}
				});
			}
		});
		
		table.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getButton() == MouseEvent.BUTTON1 &&
						(e.getModifiers() & ActionEvent.CTRL_MASK)!=ActionEvent.CTRL_MASK &&
						(e.getModifiers() & ActionEvent.SHIFT_MASK)!=ActionEvent.SHIFT_MASK) {
					SwingUtilities.invokeLater(new Runnable() {
						@Override
						public void run() {
							handler.hide();	
						}
					});
				}
			}
		});
		
		//Do nothing on pressing enter - otherwise it moves selection down one row
		Action jpropellerNullAction = new AbstractAction() {
			@Override public void actionPerformed(ActionEvent e) {}
		};
		table.getInputMap(JInternalFrame.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
			.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "jpropellerNullAction");
		table.getInputMap(JInternalFrame.WHEN_FOCUSED)
			.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "jpropellerNullAction");
		table.getActionMap().put("jpropellerNullAction", jpropellerNullAction);

		if (!displayHeader) {
			listView.removeHeader();
		}
		
		helper = new CompositeViewHelper(views);
	}



	@Override
	public JComponent getComponent() {
		return button;
	}

	@Override
	public boolean selfNaming() {
		return false;
	}
	
	/**
	 * The {@link ListAndSelectionAndValueReference} used by this view - this can
	 * be used to monitor the selected value
	 * @return	{@link ListAndSelectionAndValueReference}
	 */
	public MultiSelectionReference<T> reference() {
		return reference;
	}

	static class PopupHandler implements PopupMenuListener {
		
		private final JPopupMenu popup;
		private final Component focusComponent;
		private final Component popupComponent;
		private final Component invoker;
		private final int minWidth;
		private final int gap = 4;
		private final Runnable preparePopup;

		public PopupHandler(Component popupComponent, Component focusComponent, Component invoker, int minWidth, Runnable preparePopup) {
			
			this.invoker = invoker;
			this.focusComponent = focusComponent;
			this.popupComponent = popupComponent;
			this.minWidth = minWidth;
			this.preparePopup = preparePopup;
			
		    popup = new JPopupMenu();
		    popup.removeAll();
		    popup.setBorder(new MatteBorder(1, 1, 1, 1, Color.darkGray));
		    popup.setLayout(new BorderLayout());
		    popup.add(popupComponent, BorderLayout.CENTER);

		    popup.addPopupMenuListener(this);
		    popup.pack();
		}
		
		public void hide() {
			popup.setVisible(false);
		}

		public void show() {
		    	int width = invoker.getWidth();
		    	if (width < minWidth) {
		    		width = minWidth;
		    	}
		    	preparePopup.run();
			    popupComponent.setPreferredSize(new Dimension(width, popupComponent.getPreferredSize().height));
			    popupComponent.setMinimumSize(new Dimension(width, popupComponent.getMinimumSize().height));
			    popupComponent.setMaximumSize(new Dimension(width, popupComponent.getMaximumSize().height));
			    popup.pack();
				
				//Find position relative to invoker - if we would appear (partially) off screen bottom, display above
				//instead of below
				int x = 0;
				int y = invoker.getHeight() + gap;
				if (invoker.getLocationOnScreen().getY() + y + popup.getHeight() > Toolkit.getDefaultToolkit().getScreenSize().height) {
					y = - popup.getHeight() - gap;
				}
				
				popup.show(invoker, x, y);
				
				//Start with correct component focused
				SwingUtilities.invokeLater(new Runnable() {
					@Override
					public void run() {
						if (focusComponent != null) {
							focusComponent.requestFocus();
						}
					}
				});
		}

		@Override 
	    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {}

	    @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
	    	if (invoker instanceof JToggleButton) {
				JToggleButton button = (JToggleButton) invoker;
				button.setSelected(false);
			}
	    }
	    @Override public void popupMenuCanceled(PopupMenuEvent e) {}		
	}
	
	//Delegates to popup table

	/**
	 * Gets the default editor of the contained {@link JTable}
	 * for a given class. See {@link JTable#getDefaultEditor(Class)}
	 * @param columnClass The edited class
	 * @return The default editor
	 */
	public TableCellEditor getDefaultEditor(Class<?> columnClass) {
		return table.getDefaultEditor(columnClass);
	}

	/**
	 * Gets the default renderer of the contained {@link JTable}
	 * for a given class. See {@link JTable#getDefaultRenderer(Class)}
	 * @param columnClass The rendered class
	 * @return The default renderer
	 */
	public TableCellRenderer getDefaultRenderer(Class<?> columnClass) {
		return table.getDefaultRenderer(columnClass);
	}

	/**
	 * Sets the default editor of the contained {@link JTable}
	 * for a given class. See {@link JTable#setDefaultEditor(Class, TableCellEditor)}
	 * @param columnClass The edited class
	 * @param editor The default editor
	 */
	public void setDefaultEditor(Class<?> columnClass, TableCellEditor editor) {
		table.setDefaultEditor(columnClass, editor);
	}

	/**
	 * Sets the default renderer of the contained {@link JTable}
	 * for a given class. See {@link JTable#setDefaultRenderer(Class, TableCellRenderer)}
	 * @param columnClass The rendered class
	 * @param renderer The default renderer
	 */
	public void setDefaultRenderer(Class<?> columnClass, TableCellRenderer renderer) {
		table.setDefaultRenderer(columnClass, renderer);
	}

	/**
	 * Sets the row height of the contained {@link JTable}
	 * @param rowHeight		The row height
	 */
	public void setRowHeight(int rowHeight) {
		table.setRowHeight(rowHeight);
	}

    /**
     * Remove header from table display
     */
    public void removeHeader() {
    	table.setTableHeader(null);
    }
	
	//JView methods delegated to helper
	@Override
	public void cancel() {
		helper.cancel();
	}

	@Override
	public void commit() throws CompletionException {
		helper.commit();
	}

	@Override
	public void dispose() {
		helper.dispose();
	}

	@Override
	public boolean isEditing() {
		return helper.isEditing();
	}

	@Override
	public void update() {
		helper.update();
	}
	
	@Override
	public Format format() {
		return Format.SINGLE_LINE;
	}
}

